package org.fhnw.aigs.client.communication; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringReader; import java.net.Socket; import java.util.Date; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javafx.application.Platform; import javax.swing.JOptionPane; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.fhnw.aigs.client.GUI.LoadingWindow; import org.fhnw.aigs.client.GUI.SetupWindow; import org.fhnw.aigs.client.GUI.SettingsWindow; import org.fhnw.aigs.client.gameHandling.ClientGame; import org.fhnw.aigs.commons.GameMode; import org.fhnw.aigs.commons.JoinType; import org.fhnw.aigs.commons.Player; import org.fhnw.aigs.commons.XMLHelper; import org.fhnw.aigs.commons.communication.BadInputMessage; import org.fhnw.aigs.commons.communication.ExceptionMessage; import org.fhnw.aigs.commons.communication.ForceCloseMessage; import org.fhnw.aigs.commons.communication.IdentificationResponseMessage; import org.fhnw.aigs.commons.communication.JoinResponseMessage; import org.fhnw.aigs.commons.communication.KeepAliveMessage; import org.fhnw.aigs.commons.communication.Message; import org.fhnw.aigs.commons.communication.NotifyMessage; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * This class is responsible for parsing and handling all messages sent to the * client. In some way it is <b>the</b> most important class. It consists of two * parts:<br> * <ul><li>Message receving</li><li>Message handling</li></ul><br> * v1.0 Initial release<br> * v1.1 Functional changes<br> * v1.2 Added new messages and depending handling * * @author Matthias Stöckli (v1.0) * @version 1.2 (Raphael Stoeckli, 23.10.2014) */ public class ClientMessageBroker implements Runnable { /** * The socket which connects the client to the server */ private static Socket socket; /** * The BufferedReader used to read the incoming messages */ private static BufferedReader in; /** * Reference to the ClientGame */ private static ClientGame clientGame; /** * The currently received message in a human readable form */ private static String currentPrettyPrintedMessage = ""; /** * Indicates whether the currently processed message is a message which is * not related to games, e.g. a JoinMessage etc. */ private boolean nonGameMessageReceived = false; /** * Initializes the ClientMessageBroker and sets up a {@link Socket}.<br> * This class is responsible for parsing, interpreting and passing messages * to the game logic (client wise). This class is the counterpart of the * <b>ServerMessageBroker</b> on the server side. * * @param socket Socket which connects the server to the client. * @param game A reference to the game */ public ClientMessageBroker(Socket socket, ClientGame game) throws IOException { ClientMessageBroker.clientGame = game; ClientMessageBroker.socket = socket; ClientMessageBroker.in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); } /** * Starts the message listening loop. */ @Override public void run() { Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.INFO, "Client ready, waiting for incoming messages..."); listenForMessages(); } /** * This method constantly listens for new inputs coming from the server. The * messages are first parsed using {@link ClientMessageBroker#parseInput}. * Then the parsed messages will be handled. System messages will be handled * by <b>ServerMessageBroker.checkForNonGameMessages</b> on the server side. * All other messages are then passed to the * <b>ServerMessageBroker.processGameLogic</b> (also server side) method. */ private void listenForMessages() { try { String inputString = ""; while ((inputString = in.readLine()) != null) { Message parsedMessage = parseInput(inputString); if (parsedMessage instanceof KeepAliveMessage == false) { printMessage(inputString); } // if (parsedMessage instanceof GameStartMessage) { // clientGame.getGameWindow().removeOverlay(); // } checkForNonGameMessages(parsedMessage); // Stop the processing if a non game message was // handled or if there is no game or the message is not valid. if (nonGameMessageReceived) { nonGameMessageReceived = false; continue; } clientGame.processGameLogic(parsedMessage); } } catch (IOException ex) { // Stops the game in the case of a lost connection. Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.SEVERE, "Lost connection to the server. Most probably the server was shut down. The game was closed."); System.exit(0); } catch (Exception ex) { // Stops the game in the case of an exception. Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.SEVERE, "An exception on the client side occured.", ex); System.exit(0); } } /** * Send the message using the currently configured socket. Use this method * whenever possible. The player will be attached and can therefore be * identified by the server by using the {@link Message#getPlayer} method. * * @param message The message to be sent. */ public static void sendMessage(Message message) { message.send(socket, clientGame.getPlayer()); } /** * This method parses the incoming messages.<br> * It first tries to create a DOMDocument in order to check whether the * string is valid xml and to determine the message type. If the message * cannot be parsed, the client will receive a {@link BadInputMessage}. If * the parsing is successful, the respective message class is loaded via the * system ClassLoader.<br> * In a last step, the {@link Message#parse} method will take care of the * parsing itself. * * @param inputString The message as received from the clients. * @return The parsed message. */ private Message parseInput(String inputString) { String messageClassPath = null; Message parsedMessage = null; Class messageClass = null; // Read MessageType for further processing StringReader reader = new StringReader(inputString); // Used to create an input source and for parsing InputSource inputSource = new InputSource(reader); // Used for DOM Parsing of message (find out the type) // Used to find out the type of message Document DOMDocument; try { DOMDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource); // Read the attribute "classPath" of the sent XML. It represents the qualified name of the class // e.g. "org.fhnw.aigs.commons.TicTacToe.FieldClickMessage" which points to the class "FieldClickMessage" messageClassPath = DOMDocument.getDocumentElement().getAttribute("FullyQualifiedClassName"); } catch (ParserConfigurationException | SAXException | IOException ex) { Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.SEVERE, "Could not parse input into xml format", ex); } catch (Exception ex) // All other exceptions { Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.SEVERE, "An unknown Error occurred.", ex); } reader = new StringReader(inputString); // Recreate the reader because the string was read with the last operation // Try to load and parse the Message Class dynamically by the name provided by the attribute "FullyQualifiedName" // which is provided by every Message. try { messageClass = Class.forName(messageClassPath); parsedMessage = Message.parse(reader, messageClass); // } catch (ClassNotFoundException ex) { Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.SEVERE, "Could not find a matching class. Check the package name, @XmlElement annotations and the jars.", ex); } catch (Exception ex) // All other Exceptions { Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.SEVERE, "An unknown error occurred.", ex); } return parsedMessage; } /** * Checks the incoming messages for the following system messages:<br><ul> * <li>{@link ForceCloseMessage}</li> * <li>{@link KeepAliveMessage}</li> * <li>{@link IdentificationResponseMessage}</li> * <li>{@link JoinResponseMessage}</li> * <li>{@link NotifyMessage}</li> * <li>{@link ExceptionMessage}</li> * <li>{@link BadInputMessage}</li> * </ul> * * @param parsedMessage The parsed message. */ private void checkForNonGameMessages(Message parsedMessage) { // Alias for clarity Message m = parsedMessage; if (m instanceof ForceCloseMessage) { handleForceCloseMessage(parsedMessage); } if (m instanceof KeepAliveMessage) { handleKeepAliveMessage(parsedMessage); } else if (m instanceof IdentificationResponseMessage) { handleIdentificationResponseMessage(parsedMessage); } else if (m instanceof ExceptionMessage) { handleExceptionMessage(parsedMessage); } else if (m instanceof BadInputMessage) { handleBadInputMessage(parsedMessage); } else if (m instanceof NotifyMessage) { handleNotifyMessage(parsedMessage); } else if (m instanceof JoinResponseMessage) { handleJoinResponseMessage(parsedMessage); } } /** * Handles a notify message from the server. It will only show a message box * * @param parsedMessage The {@link NotifyMessage}. * @since v1.2 */ private void handleNotifyMessage(Message parsedMessage) { NotifyMessage notifyMessage = (NotifyMessage) parsedMessage; JOptionPane.showMessageDialog(null, notifyMessage.getMessage(), "Message from AIGS server", JOptionPane.INFORMATION_MESSAGE); nonGameMessageReceived = true; // Was handled } /** * Handles a message from the server after a joining operation.<br> * A Message box will only appear if the state is false * * @param parsedMessage The {@link JoinResponseMessage}. * @since v1.2 */ private void handleJoinResponseMessage(Message parsedMessage) { JoinResponseMessage joinResponse = (JoinResponseMessage) parsedMessage; if (joinResponse.getJoinState() == false) { String caption; if (joinResponse.getJoinType() == JoinType.CreateNewGame) { caption = "Party could not be created"; } else if (joinResponse.getJoinType() == JoinType.JoinParticularGame) { caption = "Party could not be joined"; } else { caption = "No party could be joined or created"; } JOptionPane.showMessageDialog(null, joinResponse.getMessage(), caption, JOptionPane.INFORMATION_MESSAGE); nonGameMessageReceived = true; // Was handled Platform.runLater(new Runnable() { // Important! UI manipulation must be handled with runLater from another tread @Override public void run() { clientGame.getGameWindow().setOverlay(new SetupWindow(clientGame)); // Show Setup window clientGame.getGameWindow().getHeader().setStatusLabelText(""); // Reset State } }); Settings.getInstance().SetGameStop(); } else //Operation successfull { if (joinResponse.getJoinType() == JoinType.Auto) { if (joinResponse.isGameCreated() == true) // Show overlay { if (joinResponse.getGameMode() == GameMode.Multiplayer) { Platform.runLater(new Runnable() { // Important! UI manipulation must be handled with runLater @Override public void run() { //clientGame.getGameWindow().removeOverlay(); clientGame.getGameWindow().setOverlay(new LoadingWindow()); // Show Waiting window clientGame.getGameWindow().getHeader().setStatusLabelText("Waiting for other players"); // Reset State } }); } else // Start Game in Single Player mode { Settings.getInstance().SetGameRunning(); } } } else { Settings.getInstance().SetGameRunning(); } } } /** * If the client needs to be closed this method informs the user about the * event and closes the window. * * @param parsedMessage The {@link ForceCloseMessage}. */ private void handleForceCloseMessage(Message parsedMessage) { ForceCloseMessage forceCloseMessage = (ForceCloseMessage) parsedMessage; JOptionPane.showMessageDialog(null, forceCloseMessage.getReason(), "Game Over", JOptionPane.WARNING_MESSAGE); System.exit(0); } /** * This method handles KeepAliveMessages by sending a KeepAliveMessage back * to the server. If the KeepAliveManager has been deactivated, this method * is not needed. * * @param parsedMessage the {@link KeepAliveMessage}. */ private void handleKeepAliveMessage(Message parsedMessage) { if (parsedMessage instanceof KeepAliveMessage) { // Do not react to the KeepAliveMessage as long as there // is no player available - without a valid player and // his or her playername, the signal cannot be processed if (clientGame.getPlayer() != null) { KeepAliveMessage keepAliveMessage = (KeepAliveMessage) parsedMessage; KeepAliveMessage responseMessage = new KeepAliveMessage(); responseMessage.setSentTime(keepAliveMessage.getSentTime()); responseMessage.setAnswerTime(new Date()); sendMessage(parsedMessage); nonGameMessageReceived = true; // Was handled } } } /** * This method handles the server's response to a client login attempt. It * creates a new user file locally, if needed and shows a prompt if the user * name has never been typed in or if the user name and password do not * match. * * @param parsedMessage The {@link IdentificationResponseMessage}. */ private void handleIdentificationResponseMessage(Message parsedMessage) { IdentificationResponseMessage identificationResponseMessage = (IdentificationResponseMessage) parsedMessage; // Check if the login was successful if (identificationResponseMessage.getLoginSuccessful() == false) { // Show the settings GUI Settings.getInstance().SetGameStop(); SettingsWindow identificationGUI = new SettingsWindow(); identificationGUI.setVisible(true); // Change the status label on the identification GUI SettingsWindow.notifyOfFailure(identificationResponseMessage.getReason()); } else { // add player based on the identification // this cannot be done earlier due to the fact that // the server may have allocated another name to the user Player player = new Player(identificationResponseMessage.getLoginName(), identificationResponseMessage.getPlayerName(), false); clientGame.setPlayer(player); Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.INFO, "Identification successful - new Player {0}", player.toString()); clientGame.onGameReady(); // Notify client game about the established connection nonGameMessageReceived = true; // Was handled } } /** * This method handles {@link ExceptionMessage}s. They are sent to the * client when an exception occured. The client is then informed about this * event. Afterwards, the client will close. * * @param parsedMessage The {@link ExceptionMessage}. */ private void handleExceptionMessage(Message parsedMessage) { JOptionPane.showMessageDialog(null, "There was an error on the server side of your game. The following stack trace should help you:\r\n" + currentPrettyPrintedMessage, "Game Over", JOptionPane.WARNING_MESSAGE); System.exit(0); } /** * This method handles {@link BadInputMessage}s. They are sent to the client * when the client sends a bad input (e.g. wrong formatted XML) The client * is then informed about this event. The user can decide whether the client * shall be shut down or not. * * @param parsedMessage The {@link BadInputMessage}. */ private void handleBadInputMessage(Message parsedMessage) { BadInputMessage badInputMessage = (BadInputMessage) parsedMessage; Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.INFO, "Server reported bad input: " + badInputMessage.getInput(), currentPrettyPrintedMessage); int result = JOptionPane.showConfirmDialog(null, "This client sent a string to the server which it could " + "not process.\n" + "The string has been stored in the logs.\n" + "This may cause unpredictable behaviour.\n" + "Do you want to proceed?", "Close the application?", JOptionPane.YES_NO_OPTION); if (result == JOptionPane.NO_OPTION) { System.exit(0); } } /** * Adds incoming message to the log file. * * @param inputString The message string. */ private void printMessage(String inputString) { currentPrettyPrintedMessage = XMLHelper.prettyPrintXml(inputString); Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.INFO, "<= \n {0}", currentPrettyPrintedMessage); } /** * This method is connected to * {@link ClientMessageBroker#handleIdentificationResponseMessage}. If the * user connects to the server for the first time, a file is being created * which stores the user's name and identification code. * @deprecated Method not used anymore. Please remove from code */ private void writeUserFile(String userName, String identificationCode, String serverAddress, String serverPort) { File userFile = new File("aigs.user"); // Check whether the file already exists. if (userFile.exists() == false) { boolean success = false; try { success = userFile.createNewFile(); try (PrintWriter pw = new PrintWriter(userFile)) { // Strip all numbers. This is necessary when the server is // running on localhost or the IsMultiLoginAllowed option // is set on true on the server side. Pattern p = Pattern.compile("(\\w*\\.\\w*)(\\d)"); Matcher m = p.matcher(userName); if (m.find()) { userName = m.group(1); } // Write the file. pw.println(userName); pw.println(identificationCode); } } catch (IOException ex) { Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.SEVERE, null, ex); JOptionPane.showMessageDialog(null, "Could not create user file. You will have to log in the next time again.", "Could not create file", JOptionPane.WARNING_MESSAGE); } catch (Exception ex) // All other exceptions { Logger.getLogger(ClientMessageBroker.class.getName()).log(Level.SEVERE, null, ex); JOptionPane.showMessageDialog(null, "An unknown error occurred while crating user file.", "Could not create file", JOptionPane.WARNING_MESSAGE); } if (success == false) { JOptionPane.showMessageDialog(null, "Could not create user file. You will have to log in the next time again.", "Could not create file", JOptionPane.WARNING_MESSAGE); } } } }